﻿// doors-core-alpha.js
//
// This library contains the doors storytelling engine.
//
// Released under the Apache 2.0 license on http://code.google.com/p/doors-engine/.
//////////////////////////////////////


// Global variables
//////////////////////////////////////

// Low-level (DOM) variables
//   -game canvas (drawing)
var gameCanvas; // The HTML5 canvas element to which doors will render.
var gameContext; // The 2D API for 'gameCanvas'.
var kin; // Wrapping 'gameCanvas' and 'gameContext' with Kinetic2D library.
//   -depth canvas (scaling)
var depthCanvas; // A hidden HTML5 canvas created by doors for the depth map.
var depthContext; // The 2D API for 'depthCanvas'.
//   -object canvas (hit-testing)
var objectCanvas; // A hidden HTML5 canvas created by doors for the object map.
var objectContext; // The 2D API for 'objectCanvas'.
//   -tooltip (hovertext)
var tooltipElement; // The HTML element to which hovertexts will render.
var previousFrameHoverObject; // As an optimization, we only render hovertext if the current object changes.

// High-level (doors) variables
var rooms; // The collection of rooms, keyed by the room name.
var currentRoomName; // The name of the current room.


// Engine functions
//////////////////////////////////////

// Initialization function
// USE:
//	Call this in "body.onLoad".
// PRE:
// 	DOM contains a 640x480 pixel canvas element with ID 'canvasId'.
// 	DOM contains a 640x480 pixel canvas element with ID 'depthCanvasId'.
// 	DOM contains a 640x480 pixel canvas element with ID 'objectCanvasId'.
//	DOM contains a text-ready element with ID 'gameTooltipId'.
// POST:
//	'kin' is the Kinetic2D wrapper for the game canvas element.
//	'gameCanvas' is the game canvas element.
//	'gameContext' is the 2D API handle for the game canvas element.
//	The canvas has been filled with a base color.
//	The 'rooms' dictionary has been initialized.
//	'depthContext' is the 2D API handle for the depth canvas element.
//  'objectContext' is the 2D API handle for the object canvas element.
function doorsInit(canvasId, depthCanvasId, objectCanvasId, gameTooltipId) {
	kin = new Kinetic_2d(canvasId);
	gameCanvas = kin.canvas;
	gameContext = kin.context;
	
	gameContext.fillStyle = "rgb(136,20,0)";
	gameContext.fillRect(0, 0, 640, 480);
	
	rooms = {};
	
	depthCanvas = document.getElementById(depthCanvasId);
	depthContext = depthCanvas.getContext("2d");
	
	objectCanvas = document.getElementById(objectCanvasId);
	objectContext = objectCanvas.getContext("2d");
	
	tooltipElement = document.getElementById(gameTooltipId);
	
	//TODO: Conditional logic to get elements for diagnostic output?  Make this asynchronous (toggling at runtime)?
}

// Start function
// USE:
//	Call this from your game when ready to start the doors engine.
// PRE:
//	The doorsInit() function has been called.
// POST:
//	The 'kin' object is set to animation mode.
//	The doorsGameLoop() function is set to run periodically to process and render frames.
function doorsStart(firstRoomName) {
	kin.setDrawStage(doorsGameLoop);

    kin.getCanvas().onmouseout = function() {
        kin.drawStage();
    };
    
    kin.getCanvas().onmouseup = doorsOnMouseUp;
    
    kin.startAnimation();
}

// Stop function
// USE:
//	Call this from your game when you want to stop the doors engine.
// PRE:
//	The doorsInit() function has been called.
// POST:
//	The 'kin' object is set to stop animating.
function doorsStop() {
	kin.stopAnimation();
}

// DImageLoadRequest: A request to load an image.
// USE:
//	Pass in an array to 'doorsLoadImages' as the 'request' parameter.
function DImageLoadRequest(source, target) {
	this.source = source;
	this.target = target;
}

// Load each source (from the array of requests) and invoke callback when done.
// Credit: http://www.html5canvastutorials.com/tutorials/html5-canvas-image-loader/
// USE:
//	Load images in a batch, invoking a single callback when all valid images are loaded.
//	Skips undefined image source URLs.
// PRE:
//	'requests' is an array of DImageLoadRequest objects.
//	'callback' is a function to be invoked upon completion, taking 'callbackArg' as an argument.
// POST:
//	'callback' is invoked with 'callbackArg' when all defined image sources have been loaded.
//	NOTE: This does not account for errors in loading images; it only filters out "undefined" URLs.
function loadImages(requests, callback, callbackArg) {
	var numImages = requests.length;
	var loadedImages = 0;

	var loadedHandler = function() {
		if (++loadedImages >= requests.length) {
			callback(callbackArg);
		}
	};

	for (var r in requests) {
		var request = requests[r];
		if (request.source != undefined) {
			var source = request.source;
			var target = request.target;
			target.onload = loadedHandler;
			target.src = source;
		} else {
			loadedHandler();
		}
	}
}

// DRoom: A room in the doors engine that can contain objects.
// USE:
//	Constructor function for a room.
// PRE:
//	'name' should be unique among all rooms.
//	'background' is a valid image file URL.  (Image map)
//	'depth' is a valid image file URL.  (Depth map)  (Required for character walking.)
//	'objects' is an array of DObject objects.
function DRoom(name, background, depth, objects) {
	this.name = name;
	this.background = background;
	this.depth = depth;
	this.objects = objects; //TODO: Handle initializing when undefined?
	
	this.loaded = false; //TODO: Single flag?
	this.objectsLoaded = 0;
	this.allObjectsLoaded = false;
}

// Developer-friendly text representation of a DRoom object.
// USE:
//	For debugging output, just "print" the room and this will be used to generate the text.
DRoom.prototype.toString = function() {
	return "DRoom: " + this.name;
}

DRoom.prototype.load = function() {
	// Skip loading if the room has already been loaded.
	if (this.loaded == true) {
//		onLoadHandler();
		return;
	}
	
	// Sort the objects by decreasing depth (for rendering with painter's algorithm).
	// This should be done whenever the objects in the room move or objects are added.
	this.objects.sort(sortObjectsByDepthDescending); //TODO: Find a better place for this?
	
	// Create a lookup map of objects based on their masks' color keys.
	this.objectLookup = new Array();
	for (var i = 0; i < this.objects.length; i++) {
		var object = this.objects[i];
		var colorKey = object.colorKey;
		if (colorKey != undefined) {
			var key = colorKey.red * 256 * 256 + colorKey.green * 256 + colorKey.blue;
			this.objectLookup[key] = object;
		}
	}
	
	// Update room status when finished loading the object.
	var callback = function(room) {
		room.loaded = true; //TODO: Wait for objects to be loaded? (Allows callback arg to 'load'!)
	}
	
	// Load the images needed by the room.
	this.backgroundImage = new Image();
	this.depthImage = new Image();
	
	var requests = [
		new DImageLoadRequest(this.background, this.backgroundImage),
		new DImageLoadRequest(this.depth, this.depthImage)
	];
	
	loadImages(requests, callback, this);

	// Load the room objects (images, masks, etc., as handled by the objects themselves).
	if (this.objects.length == 0) {
		this.allObjectsLoaded = true;
	} else {
		for (var i = 0; i < this.objects.length; i++) {
			var object = this.objects[i]; 
			if (object != undefined) {
				object.load(function(room) {
			        if (++(room.objectsLoaded) >= room.objects.length) {
			            room.allObjectsLoaded = true;
			        }
				}, this);
			} else {
				this.objectsLoaded++;
			}
		}
	}

	// Add the room to the global dictionary for later reference.
	rooms[this.name] = this;
}

// Render the room, including characters and objects.
// USE:
//	Called from doorsGameLoop() for the current room.
// PRE:
//	'doorsStart' has been called.
//	'load' has been called on the room.
// POST:
//	The room's background and objects have been rendered.
DRoom.prototype.render = function() {
	// TODO: Sort objects by depth (if moving?) - Optimize to just bubble-sort the moving objects?
	
	// Render the room background and objects if they have all finished loading.
	if (this.loaded && this.allObjectsLoaded) {
		gameContext.drawImage(this.backgroundImage, 0, 0);
		for (var i = 0; i < this.objects.length; i++) {
			var object = this.objects[i];
			if (object != undefined) {
				object.render();
			}
		}
	}
	
	// Render the room depth map.  Only needs to occur on room changes.
	if (this.loaded) {
		depthContext.clearRect(0, 0, 640, 480);
		depthContext.drawImage(this.depthImage, 0, 0);
	}
	
	// Render the room object map.
	if (this.allObjectsLoaded) { //TODO: Handle loading differently!
		for (var i = 0; i < this.objects.length; i++) {
			var object = this.objects[i];
			if (object != undefined) {
				object.renderMask();
			}
		}
	}
}

// DObject: A basic object in the doors engine.
// USE:
//	Constructor function for an object.
// PRE:
//	'name' should be unique among all objects.
//	'image' is a valid image file URL.  (Image map)
//	'mask' is a valid image file URL.  (Object map)  (Required for hit testing.)
//	'colorKey' is an array with red, green, and blue values matching the mask color.  (Required for hit testing.)
//	'hoverText' is a string to be displayed on mouse-over.  (Optional)
//	'x' is the object's initial x position (regardless of room).
//	'y' is the object's initial y position (regardless of room).
//	'depth' is the object's initial depth (regardless of room).
function DObject(name, image, mask, colorKey, hoverText, x, y, depth) {
	this.name = name;
	this.image = image;
	this.mask = mask;
	this.colorKey = colorKey;
	this.hoverText = hoverText;
	this.x = x;
	this.y = y;
	this.depth = depth;
	
	this.loaded = false;
}

// Developer-friendly text representation of a DObject object.
// USE:
//	For debugging output, just "print" the object and this will be used to generate the text.
DObject.prototype.toString = function() {
	return "DObject: " + this.name;
}

// Load the object (e.g., download images).
// USE:
//	Must be called before the object can be rendered in the render loop.
//	Can be customized by objects inheriting from DObject, e.g. to load multi-frame animations.
// PRE:
//	The basic DObject contains an 'image' and a 'mask' (both optional) as image URLs to load.
// POST:
//	The basic DObject contains an 'imageFile' and a 'maskFile' object (both optional) for rendering.
DObject.prototype.load = function(onLoadHandler, onLoadHandlerArg) {
	// Skip loading if the object has already been loaded.
	if (this.loaded == true) {
		onLoadHandler(onLoadHandlerArg);
		return;
	}
	
	// Update object status and call 'onLoadHandler' when finished loading the object.
	var callback = function(object) {
		object.loaded = true;
		onLoadHandler(onLoadHandlerArg);
	}
	
	// Load the images needed by this object.
	this.imageFile = new Image();
	this.maskFile = new Image();
	
	var requests = [
		new DImageLoadRequest(this.image, this.imageFile),
		new DImageLoadRequest(this.mask, this.maskFile)
	];
	
	loadImages(requests, callback, this);
}

// Renders an object's image map to the image buffer.
// USE:
//	Used in the render loop to display the object.
//	You may override this function to render your object differently (e.g., by displaying a frame from an animation).
// PRE:
//	'doorsInit' has been called.
//	The object has been loaded, including the 'imageFile' image.
DObject.prototype.render = function() {
	if (this.image != undefined && this.loaded == true) {
		gameContext.drawImage(this.imageFile, this.x, this.y);
	}
}

// Renders an object's object mask to the object buffer.
// USE:
//	Used in the render loop to update the object's hit test data in the object buffer.
//	You may override this function to render your object differently (e.g., by displaying a frame from an animation).
// PRE:
//	'doorsInit' has been called.
//	The object has been loaded, including the 'maskFile' image.
DObject.prototype.renderMask = function() {
	if (this.mask != undefined && this.loaded == true) {
		objectContext.drawImage(this.maskFile, this.x, this.y);
	}
}

// Sort function for comparing two DObject objects.  See http://www.w3schools.com/jsref/jsref_sort.asp
// USE:
//	Sort DObject objects in order of descending depth (e.g., for rendering via painter's algorithm).
// PRE:
//	'a' is a DObject object with the doors standard members.
//	'b' is a DObject object with the doors standard members.
function sortObjectsByDepthDescending(a, b) {
	return b.depth - a.depth;
}

// Navigate to the specified doors room.
// USE:
//	Change the current room from your game scripts.
// PRE:
//	'room' is a room object with the doors standard members.
// POST:
//	'currentRoomName' is set to 'room.name'.
//	Future game loops will render the new current room.
function doorsGoToRoom(room) { //TODO: Asynchronicity - should this block doorsGameLoop?
	currentRoomName = room.name;
	
	// Wait for room to load.
	//TODO: Do this properly (with a to-load list and progress bar, etc.)!
/*	waitFor(function() {
		var room = rooms[currentRoomName];
		return room.depthLoaded;
	}, function() {
		// Render the room depth map.  Only needs to occur on room changes.
		var room = rooms[currentRoomName];
		depthContext.clearRect(0, 0, 640, 480);
		depthContext.drawImage(room.depthImage, 0, 0);
	}, room);*/
}

// Wait for a predicate to be true, then execute the future.
function waitFor(predicate, future) { //TODO: Use a standard Promises library to manage asynchrony?
	if (predicate()) {
		future();
	} else {
		setTimeout(waitFor(predicate, future), 20);
	}
}

// The main game loop for the doors engine, including rendering.
// USE:
//	Set to run at a given interval from the doorsStart() function.
// PRE:
//	The doorsInit() function has been called.
//	The doorsStart() function has been called.
function doorsGameLoop() {
	var mousePos = kin.getMousePos();
	
	gameContext.clearRect(0, 0, 640, 480);
	objectContext.clearRect(0, 0, 640, 480);

	var room = rooms[currentRoomName];
	if (room != undefined) {
		room.render(room);
	}
	
	updateHoverText(room, mousePos);
	//printDiagnosticInfo(room, mousePos);
}

// Print out the hovertext for the object currently under the mouse, if any.
// USE:
//	Called from doorsGameLoop().
// PRE:
//	For useful results, 'mousePos' must be a valid value.
//	For useful results, the room's object map must have been properly populated.
function updateHoverText(room, mousePos) {
	if (mousePos != undefined) {
		var mouseX = mousePos.x;
		var mouseY = mousePos.y;
		
		// Print out the hovertext for the object currently under the mouse cursor, if any.
		// As an optimization, only update the hovertext if the current object changes.
		var object = getObjectAtCoordinate(mousePos.x, mousePos.y, room);
		if (object != previousFrameHoverObject) {
			if (object != undefined) {
				tooltipElement.innerHTML = object.hoverText;
			} else {
				tooltipElement.innerHTML = '(not a hotspot)';
			}
			previousFrameHoverObject = object;
		}
	}
}

// Print out diagnostic info to specific DOM elements.
// USE:
//	Called from doorsGameLoop().
// PRE:
//	(Optional) DOM elements with ID "x-counter" and "y-counter" for printing mouse coordinates.
//	(Optional) DOM element with ID "depth-counter" for printing depth value at mouse coordinates.
function printDiagnosticInfo(room, mousePos) {
	// Print out the in-canvas mouse coordinates, if known.
	if (mousePos != undefined) {
		var mouseX = mousePos.x;
		var mouseY = mousePos.y;
		$('#x-counter').html(mouseX);
		$('#y-counter').html(mouseY);
		
		// Print out the depth value at the mouse cursor location.
		var depth = getDepthAtCoordinate(mousePos.x, mousePos.y);
		if (depth != undefined) {
			$('#depth-counter').html(depth);
		} else {
			$('#depth-counter').html('(not walkable)');
		}
	}
	
	$('#counter-fps').html(kin.getFps());
}

// Handle mouse click events on the canvas.
// USE:
//	Called as an event handler for mouse-up events on the game canvas.
// PRE:
//	'doorsInit' has been called.
//	A current room has been set.
// POST:
//	If an object with an 'onclick' handler was clicked, its handler is invoked with arguments:
//		-mouseX
//		-mouseY
//		-depth
//		-object
function doorsOnMouseUp() {
	var room = rooms[currentRoomName];
	var clickEventInfo = {};
	
	var mousePos = kin.getMousePos();
	clickEventInfo.mousePos = mousePos;
	
	if (mousePos != undefined) {
		var mouseX = mousePos.x;
		var mouseY = mousePos.y;
		var depth = getDepthAtCoordinate(mouseX, mouseY);
		var object = getObjectAtCoordinate(mouseX, mouseY, room);
		
		clickEventInfo.mouseX = mouseX;
		clickEventInfo.mouseY = mouseY;
		clickEventInfo.depth = depth;
		clickEventInfo.object = object;
		
		if (object != undefined && object.onclick != undefined) {
			object.onclick(clickEventInfo);
		}
	}

	$('#event-history').append('<li>CLICK! ' +
		'(' + clickEventInfo.mouseX + ',' + clickEventInfo.mouseY + ',' + clickEventInfo.depth + '): [' +
		clickEventInfo.object + ']</li>');
}

// Get the depth value at a specified coordinate.
// USE:
//	Can be called to determine the current depth at a given X-Y coordinate.
// PRE:
//	'doorsInit' has been called.
//	For meaningful results, 'depthContext' must have completed rendering the current room's depth map.
//	For meaningful results, the current room's depth map must follow the depth map standard.
// RETURNS:
//	The depth (0 = foreground, 255 = max distance) at the given X-Y coordinate, or undefined if that point is not walkable.
function getDepthAtCoordinate(x, y) {
	var depthData = depthContext.getImageData(0, 0, 640, 480);
	var data = depthData.data;

	// Just get the red (all colors are equal for grayscale depth maps) and the alpha (for walkable check).
	var red = data[((640 * y) + x) * 4];
	var alpha = data[((640 * y) + x) * 4 + 3];
	
	if (alpha == 255) {
		return 255 - red; // Standard depth maps have white (as depth 0) in the foreground; be consistent.
	} else {
		return undefined;
	}
}

// Get the object at a specified coordinate.
// USE:
//	Can be called to determine the current object at a given X-Y coordinate.
// PRE:
//	'doorsInit' has been called.
//	For meaningful results, 'objectContext' must have completed rendering the current room's object map.
//	For meaningful results, the current room's object map must follow the object map standard.
//	For values to be returned, the current room's 'objectLookup' table must have been initialized.
// RETURNS:
//	The object at the given X-Y coordinate, or undefined if no object is located there.
function getObjectAtCoordinate(x, y, room) {
	var objectData = objectContext.getImageData(0, 0, 640, 480);
	var data = objectData.data;

	// Get RGB (for object identity) and alpha (for hit mask).
	var red = data[((640 * y) + x) * 4];
	var green = data[((640 * y) + x) * 4 + 1];
	var blue = data[((640 * y) + x) * 4 + 2];
	var alpha = data[((640 * y) + x) * 4 + 3];
	
	if (alpha == 255) {
		var colorKey = red * 256 * 256 + green * 256 + blue;
		return room.objectLookup[colorKey];
	} else {
		return undefined;
	}
}
